/* Copyright (c) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.groupbooks.server.security;

import com.google.gdata.client.http.AuthSubUtil;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.GeneralSecurityException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Acts as a proxy and retrieves the requested feed.
 * <p>
 * The identity of the user is determined using the provided authentication
 * cookie. The authentication cookie will be mapped to the associated Google
 * AuthSub token. The token will be used to retrieve the feed.
 * <p>
 * Special care must be taken to ensure the following: - the requested feed
 * belongs to a pre-specified approved list - since cookies are used and GData
 * URLs aren't protected, the URL being requested by the client should contain a
 * secure hash. This is primarily required for POST/PUT/DELETE but shown in the
 * GET example below as an example.
 * 
 * 
 */
public class RetrieveFeedServlet extends HttpServlet {

	private static String[] acceptedFeedPrefixList = {
			"http://www.google.com/books/feeds",
			"https://www.google.com/books/feeds" };

	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		// Retrieve the authentication cookie to identify user
		String principal = Utility.getCookieValueWithName(req.getCookies(),
				Utility.LOGIN_COOKIE_NAME);
		if (principal == null) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"Unidentified principal.");
			return;
		}

		// Check that the user has an AuthSub token
		String authSubToken = TokenManager.retrieveToken(principal);
		if (authSubToken == null) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"User isn't authorized through AuthSub.");
			return;
		}

		// If no query string, complain
		if (req.getQueryString() == null) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"Query string is required.");
			return;
		}

		// Parse the query parameters
		String queryString = URLDecoder.decode(req.getQueryString(), "UTF-8");
		Map<String, String> queryParams = Utility.parseQueryString(queryString);

		String queryUri = queryParams.get("href");
		String token = queryParams.get("token");
		String timestamp = queryParams.get("timestamp");
		if ((queryUri == null) || (token == null) || (timestamp == null)) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"Missing a query parameter.");
			return;
		}

		// Verify the feed by checking that it's from a known list of feeds and
		// that the secure token hasn't expired and is valid.
		if (!verifyFeedRequest(principal, queryUri, token, timestamp, "GET")) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"Request failed validation.");
			return;
		}

		// Handle a GET request
		handleGetRequest(req, resp, queryUri, authSubToken);
	}

	/**
	 * Handles a GET request by issuing a GET to the requested feed with the
	 * AuthSub token attached in a header. The output from the server will be
	 * proxied back to the requestor. POST/PUT/DELETE can be handled in a
	 * similar manner except that the XML sent as part of the request should be
	 * sent to the server.
	 */
	private void handleGetRequest(HttpServletRequest req,
			HttpServletResponse resp, String queryUri, String authSubToken)
			throws ServletException, IOException {
		HttpURLConnection connection = null;
		try {
			connection = openConnectionFollowRedirects(queryUri, authSubToken);
		} catch (GeneralSecurityException e) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"Error creating authSub header.");
			return;
		} catch (MalformedURLException e) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
					"Malformed URL - " + e.getMessage());
			return;
		} catch (IOException e) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "IOException - "
					+ e.getMessage());
			return;
		}

		int respCode = connection.getResponseCode();

		// Handle error from remote server
		if (respCode != HttpServletResponse.SC_OK) {
			Map<String, List<String>> headers = connection.getHeaderFields();
			StringBuffer errorMessage = new StringBuffer(
					"Failed to retrive calendar feed from: ");
			errorMessage.append(queryUri);
			errorMessage.append(".\nServer Error Response:\n");
			errorMessage.append(connection.getResponseMessage());

			for (Iterator<String> iter = headers.keySet().iterator(); iter
					.hasNext();) {
				String header = iter.next();
				List<String> headerValues = headers.get(header);

				for (Iterator<String> headerIter = headerValues.iterator(); headerIter
						.hasNext();) {
					String headerVal = headerIter.next();
					errorMessage.append(header + ": " + headerVal + ", ");
				}
			}

			resp.sendError(respCode, errorMessage.toString());
			return;
		}

		// Handle success reply from remote server
		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(
					connection.getInputStream()));
			String line;
			while ((line = reader.readLine()) != null) {
				resp.getWriter().write(line);
			}
		} catch (IOException e) {
			// Ignore
		}
	}

	/**
	 * Open a HTTP connection to the provided URL with the AuthSub token
	 * specified in the header. Follow redirects returned by the server - a new
	 * AuthSub signature will be computed for each of the redirected-to URLs.
	 */
	private HttpURLConnection openConnectionFollowRedirects(String urlStr,
			String authSubToken) throws MalformedURLException,
			GeneralSecurityException, IOException {
		boolean redirectsDone = false;
		HttpURLConnection connection = null;
		while (!redirectsDone) {
			URL url = new URL(urlStr);
			// Open connection to requested feed
			connection = (HttpURLConnection) url.openConnection();
			connection.setRequestProperty("Content-Type",
					"application/x-www-form-urlencoded");

			// Form AuthSub authentication header
			String authHeader = null;
			authHeader = AuthSubUtil.formAuthorizationHeader(authSubToken,
					Utility.getPrivateKey(), url, "GET");
			connection.setRequestProperty("Authorization", authHeader);
			connection.setInstanceFollowRedirects(false);
			int responseCode = connection.getResponseCode();
			if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
					|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
				urlStr = connection.getHeaderField("Location");
				// If "Location" is not specified, stop following redirects, and
				// propagate error to the client of the proxy
				if (urlStr == null) {
					redirectsDone = true;
				}
			} else {
				redirectsDone = true;
			}
		}
		return connection;
	}

	/**
	 * Verifies the request for a feed by: a. validating that the request
	 * belongs to a known list of feeds b. validating the token (to protect
	 * against url command attacks)
	 *<p>
	 * This verification is in order to prevent the proxy from URL command
	 * attacks which is a cross site scripting problem.
	 */
	private boolean verifyFeedRequest(String cookie, String feed, String token,
			String timestamp, String method) {
		// Check the list of accepted feed URLs that we are proxying
		int url_i;
		for (url_i = 0; url_i < acceptedFeedPrefixList.length; url_i++) {
			if (feed.toLowerCase().startsWith(
					acceptedFeedPrefixList[url_i].toLowerCase())) {
				break;
			}
		}
		if (url_i == acceptedFeedPrefixList.length) {
			return false;
		}

		return SecureUrl.isTokenValid(token, cookie, feed, method, timestamp);
	}
}
